如果是很早之前就開始開發 Android APP 的話,應該對 onActivityResult
都熟到不能再熟。當初 Activity
被設計成是畫面的最小單位,一個 APP 中可以有多個 Acitivity
提供不同的功能。而不同的 Activity
之間可以藉由 intent
叫起其他的 Activity
,讓它達成工作後,再把結果帶回前一個 Activity
。這時,利用的就是這個 onActivityResult
的 callback 函式。這種跨 Activity
的溝通機制,甚至可以應用在不同 APP 之間的互動。比方說,A 應用程式叫起 B 應用程式提供的 photo picker Activity,在 B 裡選完照片後,再把照片 resource uri 返回給 A 做後續的處理。
曾幾何時, onActivityResult
因為很多因素的考量,漸漸地被廢棄了。這一篇文章要來說說,當 onActivityResult
不再是正解時,我們要怎麼完成同樣的功能。
照慣例,我們用 EinkBro 中的應用場景來解釋。
當使用者覺得網頁字型不是自己喜歡的樣式,想要選擇一個自己放入系統中的字型檔案,EinkBro 會呼叫系統的 file picker 起來;等使用者選完檔案後,file picker 會將結果送回給 EinkBro 的 Activity
。我們來看這個功能要怎麼實作。
在 BrowserUnit 中,先定義好註冊 AcitivityResult 的函式。在一般情況下,如果沒有特殊需求,我們都會使用 ActivityResultContracts.StartActivityForResult()
這個預設的 Contract,然後再最後的 {} 可以看到:當結果回來時,會在 handleFontSelectionResult()
處理字型的 content uri (將它存入 SharedPreferences)。
另外,這個函式會回傳 ActivityResultLauncher
,我們會在需要的時候呼叫它。
fun registerCustomFontSelectionResult(fragment: Fragment): ActivityResultLauncher<Intent> =
fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { handleFontSelectionResult(fragment.requireContext(), it) }
private fun handleFontSelectionResult(context: Context, activityResult: ActivityResult) {
if (activityResult.data == null || activityResult.resultCode != Activity.RESULT_OK) return
val uri = activityResult.data?.data ?: return
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
context.contentResolver?.takePersistableUriPermission(uri, takeFlags)
val file = File(uri.path)
config.customFontInfo = CustomFontInfo(file.name, uri.toString())
}
在 BrowserActivity
中,我們先呼叫 1 中的函式,將產生的 ActivityResultLauncer
留著備用。
private val customFontResultLauncher: ActivityResultLauncher<Intent> =
BrowserUnit.registerCustomFontSelectionResult(this)
然後,在下面的實作中,我們使用了它。可以看到在呼叫 resultLauncher.launch()
時,需要帶入一個 Intent
。這個 Intent
就是之前用來帶入 startActivityForResult()
的參數。
// BrowserActivity
private fun openCustomFontPicker() = BrowserUnit.openFontFilePicker(customFontResultLauncher)
// BrowserUnit
fun openFontFilePicker(resultLauncher: ActivityResultLauncher<Intent>) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = Constants.MIME_TYPE_ANY
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
resultLauncher.launch(intent)
}
當系統在進行一些比較吃記憶體的行為時(像是開相機拍照),很有可能你的 process 或 Activity
會因為記憶體不足而被系統砍掉,造成回傳的結果就收不到了。
新個流程雖然比原先的作法複雜了些,又是 Contract 又是 Launcher 的,多了許多新元件。但這麼做的好處是:它把回傳結果的流程和啟動其他 Activity
的實作切開來,避免上面的問題發生。
來自官網的說明:
When starting an activity for a result, it is possible (and, in cases of memory-intensive operations such as camera usage, almost certain) that your process and your activity will be destroyed due to low memory.
For this reason, the Activity Result APIs decouple the result callback from the place in your code where you launch the other activity. As the result callback needs to be available when your process and activity are recreated, the callback must be unconditionally registered every time your activity is created, even if the logic of launching the other activity only happens based on user input or other business logic.
建議把生成的 ActivityResultLauncher
包在 lifecycle 中,讓 lifecycle 幫忙處理生命週期。如果無法取得 lifecycle 的話,也可以在不需要時,自行呼叫 ActivityResultLauncher
的 unregister()
。